﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom.Compiler;
using System.Reflection;
using MethodWorx.CMS.TemplateLanguage;
using MethodWorx.CMS.Content;
using MethodWorx.CMS.TemplateLanguage.Design;
using MethodWorx.CMS.TemplateLanguage.Properties;
using MethodWorx.CMS.Web;
using MethodWorx.Core.DomainModel;
using MethodWorx.CMS.Plugins;
using MethodWorx.CMS.Providers;

namespace MethodWorx.CMS.TemplateParser.Compiler
{
    public class TemplateCompiler : ITemplateCompilationProvider
    {
        private ParsingEngine parser;
        private ICmsDataProvider provider;
       

        public TemplateCompiler(ICmsDataProvider provider)
        {
            this.provider = provider;

        }

        public TemplateCompiler(ParsingEngine parser)
        {
            this.provider = parser.GetProvider();
            this.parser = parser;
        }

        public object CompileAndCreateInstance(CmsRequestContext context, Func<Type, object> dependencyResolver, string code)
        {
            CodeDomProvider provider = new Microsoft.CSharp.CSharpCodeProvider();

            CompilerParameters parameters;
            parameters = new CompilerParameters();
            parameters.GenerateInMemory = true;
            string typeName = "template_" + DateTime.Now.Ticks.ToString();

            CompilerResults results;
            parameters.ReferencedAssemblies.Add("System.dll");
            parameters.ReferencedAssemblies.Add("System.Core.dll");
            parameters.ReferencedAssemblies.Add(typeof(AggregateRoot).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(Page).Assembly.Location);
            parameters.ReferencedAssemblies.Add(this.GetType().Assembly.Location);
            //var code = this.ParseToCode(typeName);
            results = provider.CompileAssemblyFromSource(parameters, code);
            if (results.Errors.HasErrors)
            {
                throw new Exception("Compilation Error: " + results.Errors[0].ToString());
            }
            Assembly assembly = results.CompiledAssembly;
            var type = assembly.GetType("templates." + typeName);

            object instance = CreateTemplateInstance(type, this.parser, context, dependencyResolver);
            return instance;
        }

        public Type Compile(string code)
        {
            CodeDomProvider provider = new Microsoft.CSharp.CSharpCodeProvider();

            CompilerParameters parameters;
            parameters = new CompilerParameters();
            parameters.GenerateInMemory = true;
            string typeName = "template_" + DateTime.Now.Ticks.ToString();

            CompilerResults results;
            parameters.ReferencedAssemblies.Add("System.dll");
            parameters.ReferencedAssemblies.Add("System.Core.dll");
            parameters.ReferencedAssemblies.Add(typeof(AggregateRoot).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(Page).Assembly.Location);
            parameters.ReferencedAssemblies.Add(this.GetType().Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(SqlServer).Assembly.Location);
            //var code = this.ParseToCode(typeName);
            results = provider.CompileAssemblyFromSource(parameters, code);
            if (results.Errors.HasErrors)
            {
                throw new Exception("Compilation Error: " + results.Errors[0].ToString());
            }
            Assembly assembly = results.CompiledAssembly;
            try
            {
                var type = assembly.GetTypes()[0];
                return type;
            }
            catch (Exception)
            {
                return null;
            }
        }

        public static object CreateTemplateInstance(Type type, ParsingEngine parser, CmsRequestContext context, Func<Type, object> dependencyResolver)
        {
            return Activator.CreateInstance(type, context, parser, dependencyResolver);
        }

        public object Compile(CmsRequestContext context, Func<Type, object> dependencyResolver)
        {
        
            CodeDomProvider provider = new Microsoft.CSharp.CSharpCodeProvider();

            CompilerParameters parameters;
            parameters = new CompilerParameters();
            parameters.GenerateInMemory = true;
            string typeName = "template_" + DateTime.Now.Ticks.ToString();

            CompilerResults results;
            parameters.ReferencedAssemblies.Add("System.dll");
            parameters.ReferencedAssemblies.Add("System.Core.dll");
            parameters.ReferencedAssemblies.Add(typeof(AggregateRoot).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(Page).Assembly.Location);
            parameters.ReferencedAssemblies.Add(this.GetType().Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(SqlServer).Assembly.Location);
            var code = this.ParseToCode(typeName);
            results = provider.CompileAssemblyFromSource(parameters, code);
            if(results.Errors.HasErrors)
            {
                throw new Exception("Compilation Error: " + results.Errors[0].ToString());
            }
            Assembly assembly = results.CompiledAssembly;
            var type = assembly.GetType("templates." + typeName);

            var instance = Activator.CreateInstance(type, context, this.parser, dependencyResolver);
            return instance;
        }

        private enum ParserResult
        {
            NotAParser,
            Parser
        }

        private void ParseElements(StringBuilder sb, TemplateNode parent, ref int parserIndex)
        {
            foreach (var element in parent.Children)
            {
                //
                //  ok, time to go through each node
                if (element is TemplateElement)
                {
                    var templateElement = element as TemplateElement;
                    bool label = false;
                    string labelName = "";

                    if(templateElement.HasAttribute("defineLabel", true))
                    {
                        labelName = templateElement.GetAttributeValue("defineLabel", null, true);
                        sb.AppendLine("Action " +  labelName  + " = null; " + labelName + " = new Action(() => {");
                        label = true;
                    }

                    var parserResult = ParserResult.NotAParser;
                    if (element.Name.Contains(":"))
                    {
                        //
                        //  ok, this is potentially a parser
                        //  so, we have to insert a bit of code to execute 
                        parserResult = this.InsertParserExecution(sb, templateElement, ref parserIndex);
                    }

                    if (parserResult == ParserResult.NotAParser)
                    {
                        if (templateElement.Name.ToLower() == "if")
                        {
                            sb.Append("if(" + this.ParseString(templateElement.GetAttributeValue("condition", "true", true), false) + ") {\r\n");
                            //  now process children
                            this.ParseElements(sb, templateElement, ref parserIndex);
                            sb.Append("}\r\n");
                        }
                        else if (templateElement.Name.ToLower() == "call")
                        {
                            sb.AppendLine(templateElement.GetAttributeValue("label", null, true) + "();");
                        }
                        else
                        {
                            sb.Append("output.Append(@\"<" + element.Name + " \");\r\n");

                            foreach (var templateAttribute in element.Children.OfType<TemplateAttribute>())
                            {
                                sb.Append("output.Append(@\"" + templateAttribute.Name + "=\"\"" +
                                          ParseString(templateAttribute.Value ?? "", true) + "\"\" \");\r\n");
                            }

                            if (element.Children.OfType<TemplateText>().Count() > 0 ||
                                element.Children.OfType<TemplateElement>().Count() > 0 ||
                                ((TemplateElement)element).HasClosingTag == true)
                            {
                                sb.Append("output.Remove(output.Length - 1, 1);\r\n");
                                sb.Append("output.Append(@\">\");\r\n");
                                this.ParseElements(sb, element, ref parserIndex);
                                sb.Append("output.Append(@\"</" + templateElement.Name + ">\");");
                            }
                            else
                            {
                                sb.Append("output.Append(@\"/>\");\r\n");
                            }
                        }
                    }

                    if(label)
                    {
                        sb.AppendLine("});");
                        sb.AppendLine(labelName + "();");
                    }
                }
                else if (element is TemplateText)
                {
                    var templateText = element as TemplateText;
                    sb.Append("output.Append(@\"" + ParseString(templateText.Text, true) + "\");\r\n");
                }
            }
        }

        public static string FormatString(object input)
        {

            if (input != null)
                return input.ToString().Replace("\"", "\"\"");
            else
            {
                return "";
            }
        }

        public static object GetVariable(Dictionary<string, object> variables, string name, Stack<object> stack)
        {
            if(name.Contains("."))
            {
                //
                //  ok, get the type name
                var type = name.Split('.')[0];
                var property = name.Split('.')[1];

                if (type.StartsWith("^"))
                {
                    type = type.TrimStart('^');
                    if (type == "")
                    {
                        //  peek of the top of the stack
                        try
                        {
                            var val = stack.Peek().GetProperty(property);
                            return val;
                        }
                        catch (Exception ex)
                        {
                            return null;
                        }
                    }
                    else
                    {
                        foreach (var item in stack)
                        {
                            var baseType = item.GetType();
                            while (baseType != null)
                            {
                                if (baseType.Name == type)
                                {
                                    //  this is the one we are after
                                    var val = item.GetProperty(property);
                                    return val;
                                }
                                baseType = baseType.BaseType;
                            }
                        }
                    }
                }
                else
                {
                    if (variables.ContainsKey(type))
                    {
                        var item = variables[type];
                        if (item != null)
                        {
                            var val = item.GetProperty(property);
                            return val;
                        }
                    }
                }

            }
            else
            {
                if (variables.ContainsKey(name))
                    return variables[name];
            }

            return null;
        }

        public static object ExecuteParser(IEditor editor, CmsRequestContext context, ParsingEngine engine, Stack<object> stack, Dictionary<string, object> variables, Dictionary<string, object> values, string parserName, string parserNamespace, Func<Type, object> dependencyResolver)
        {
            var parser = engine.GetParser(parserName, parserNamespace);
            if (parser != null)
            {

                //
                //  lets configure the parser
                foreach (var attribute in values.Keys)
                {
                    foreach (var property in parser.GetType().GetProperties())
                    {
                        bool set = false;
                        foreach (var attr in property.GetCustomAttributes(true))
                        {
                            if (attr is PropertyAttribute)
                            {
                                var pa = attr as PropertyAttribute;
                                if (string.IsNullOrEmpty(pa.Alias))
                                    pa.Alias = property.Name;
                                if (pa.Alias.ToLower() == attribute.ToLower())
                                {
                                    object converted;
                                    if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                                    {
                                        var type = property.PropertyType.GetGenericArguments()[0];
                                        converted = Convert.ChangeType(values[attribute], type);
                                    }
                                    else
                                        converted = Convert.ChangeType(values[attribute], property.PropertyType);
                                    property.SetValue(parser, converted, null);
                                    set = true;
                                    break;
                                }
                            }
                        }
                        if (set)
                            break;
                    }
                }

                foreach (var property in parser.GetType().GetProperties())
                {

                    foreach (var attr in property.GetCustomAttributes(true))
                    {
                        if (attr is DependencyPropertyAttribute)
                        {
                            var dependency = dependencyResolver(property.PropertyType);
                            if (dependency != null)
                                property.SetValue(parser, dependency, null);
                        }
                    }
                }

                var result = parser.Parse(context, stack, values);

                //
                //  ok, lets set its properties
                if(values.ContainsKey("var"))
                {
                    //  ok, its a value
                    if(variables.ContainsKey(values["var"].ToString()))
                    {
                        variables[values["var"].ToString()] = result;
                    }
                    else
                    {
                        variables.Add(values["var"].ToString(), result);
                    }
                    return null;
                }
                else
                {
                    if (parser is IEditable)
                    {
                        //
                        //  ok, if we are editable
                        ((IEditable)parser).Edit(editor);
                    }
                    return result;
                }
            }
            else
                return null;
        }

        private ParserResult InsertParserExecution(StringBuilder sb, TemplateElement element, ref int parserIndex)
        {
            var p = this.parser.GetParser(element, null);
            if (p != null)
            {
                //
                //  ok, we have to output some stuff
                sb.Append("values = new Dictionary<string, object>();\r\n");
                foreach(var attribute in element.Children.OfType<TemplateAttribute>())
                {
                    //
                    //  ok, what do we do here
                    sb.Append("values.Add(\"" + attribute.Name + "\",@\"" + this.ParseString(attribute.Value, false) + "\");\r\n");
                }
                sb.Append("parserName = \"" + element.LocalName + "\"\r\n;");
                sb.Append("parserNamespace = \"" + element.Namespace + "\"\r\n;");
                var resultName = "parserResult" + parserIndex.ToString();
                parserIndex++;
                sb.Append(
                    "var " + resultName + " = MethodWorx.CMS.TemplateParser.Compiler.TemplateCompiler.ExecuteParser(this, this.context, this.parsingEngine, stack, variables,  values, parserName, parserNamespace, this.dependencyResolver);\r\n");
                sb.Append("if(" + resultName + " is IEnumerable && !(" + resultName + " is string)) {\r\n");
                sb.Append("if(((IEnumerable)" + resultName + ").Cast<object>().Count() == 0) {\r\n");
                var notExists = element.GetElement("notexists");
                if(notExists != null)
                {
                    this.ParseElements(sb, notExists, ref parserIndex);
                }
                sb.Append("}else{\r\n");
                sb.Append("foreach(var " + resultName + "_e in (IEnumerable)" + resultName + ") {\r\n");
                sb.Append("var " + resultName + "_er = " + resultName + "_e as EnumerableResult;\r\n");
                sb.Append("if(" + resultName + "_er != null) {\r\n");
                sb.Append("foreach(var o in " + resultName + "_er.Results) { stack.Push(o); };\r\n");
                //
                //  now parse the children
                //this.ParseElements(sb, element, ref parserIndex);
                //
                //  ok, lets match them up
                foreach(var childElement in element.Children.OfType<TemplateElement>())
                {
                    sb.Append("if(" + resultName + "_er.Names.Contains(\"" + childElement.Name + "\")) {\r\n");
                    this.ParseElements(sb, childElement, ref parserIndex);
                    sb.Append("}\r\n");

                    if (childElement.Children.OfType<TemplateElement>().Where(t => t.Name == "break").Any())
                        sb.Append("else");
                }
                sb.Append(" if(false) { }\r\n");
                //  and close the brackets
                sb.Append("for(var c = 0; c < " + resultName + "_er.Results.Length; c++) { stack.Pop(); }\r\n");
                sb.Append("}else{\r\n");
                sb.Append("stack.Push(" + resultName + "_e);\r\n");

                foreach (var childElement in element.Children.OfType<TemplateElement>())
                {
                    if (childElement.Name == "exists")
                    {
                        this.ParseElements(sb, childElement, ref parserIndex);
                    }
                }
                sb.Append("stack.Pop();\r\n");
                sb.Append("}\r\n");
                sb.Append("}\r\n");
                sb.Append("}\r\n");
                sb.Append("}else{\r\n");
                /*if(element.Children.OfType<TemplateElement>().Count() > 0 || element.Children.OfType<TemplateText>().Count() > 0)
                {
                    sb.Append("stack.Push(" + resultName + ");\r\n");
                    this.ParseElements(sb, element, ref parserIndex);
                    sb.Append("stack.Pop();\r\n");
                }
                else
                {
                    //
                    //  ok, its just a value
                    //  so we just write it out*/
                    sb.Append("if(" + resultName + " != null) {\r\n");
                    sb.Append("if(" + resultName + " is HtmlResult) {\r\n");
                    sb.Append("output.Append(((HtmlResult)" + resultName + ").PreContent.ToString());\r\n");
                    sb.Append("if(((HtmlResult)" + resultName + ").Content != null){\r\n");
                    sb.Append("stack.Push(((HtmlResult)" + resultName + ").Content);\r\n");
                    sb.Append("}\r\n");
                    if (element.Children.OfType<TemplateElement>().Count() > 0 || element.Children.OfType<TemplateText>().Count() > 0)
                    {
                        this.ParseElements(sb, element, ref parserIndex);
                    }
                    else
                    {
                        sb.Append("if(((HtmlResult)" + resultName + ").Content != null){\r\n");
                        sb.Append("output.Append(((HtmlResult)" + resultName + ").Content.ToString());\r\n");
                        sb.Append("}\r\n");
                        
                    }
                    sb.Append("if(((HtmlResult)" + resultName + ").Content != null){\r\n");
                    sb.Append("stack.Pop();\r\n");
                    sb.Append("}\r\n");
                    sb.Append("output.Append(((HtmlResult)" + resultName + ").PostContent.ToString());\r\n");
                    sb.Append("}else{\r\n");
                    if(element.Children.OfType<TemplateElement>().Count() > 0 || element.Children.OfType<TemplateText>().Count() > 0)
                    {
                        sb.Append("stack.Push(" + resultName + ");\r\n");
                        this.ParseElements(sb, element, ref parserIndex);
                        sb.Append("stack.Pop();\r\n");
                    }
                    else
                        sb.Append("output.Append(" + resultName + ".ToString());\r\n");
                    sb.Append("}\r\n");
                    sb.Append("}\r\n");
                //
                sb.Append("}\r\n");

             
                //  we are a parser
                return ParserResult.Parser;
            }
            else
                return ParserResult.NotAParser;
        }


        private string ParseString(string text, bool withQuotes)
        {
            StringBuilder sb = new StringBuilder();
            if (text.Contains("{") && text.Contains("}"))
            {
                bool inProperty = false;
                string propertyName = "";
                foreach (var c in text)
                {
                    if (c == '{')
                    {
                        if (inProperty)
                        {
                            sb.Append("{" + propertyName);
                            propertyName = "";
                        }
                        inProperty = true;
                    }
                    else if (inProperty)
                    {
                        if (c == '}')
                        {
                            inProperty = false;
                            if (withQuotes)
                            {
                                sb.Append("\" + ");
                            }
                            if (propertyName.StartsWith("N:"))
                            {
                                propertyName = propertyName.Substring(2);
                                sb.Append("N(");
                            }
                            else
                                sb.Append("(");
                            sb.Append(
                                    "MethodWorx.CMS.TemplateParser.Compiler.TemplateCompiler.GetVariable(variables, \"" +
                                    propertyName + "\", stack))");
                            if (withQuotes)
                                sb.Append(" + @\"");

                            propertyName = "";
                        }
                        else
                            propertyName += c;
                    }
                    else
                    {
                        sb.Append(c);
                        if (c == '\"')
                            sb.Append(c);
                    }
                }

                if (inProperty)
                    sb.Append("{" + propertyName);
            }
            else
                return text.Replace("\"", "\"\"");

            return sb.ToString();
        }

        public string ParseToCode(string typeName)
        {
            StringBuilder sb = new StringBuilder();

            sb.Append("using System;\r\n");
            sb.Append("using System.Text;\r\n");
            sb.Append("using System.Linq;\r\n");
            sb.Append("using System.Collections;\r\n");
            sb.Append("using System.Collections.Generic;\r\n");
            sb.Append("using MethodWorx.CMS.TemplateParser;\r\n");
            sb.Append("using MethodWorx.CMS.TemplateLanguage;\r\n");
            sb.Append("using MethodWorx.CMS.Web;\r\n");
            sb.Append("namespace templates {\r\n");
            sb.Append("public class " + typeName + " : MethodWorx.CMS.TemplateLanguage.Design.IEditor {\r\n");
            sb.Append("private ParsingEngine parsingEngine;\r\n");
            sb.Append("private CmsRequestContext context;\r\n");
            sb.Append("private Stack<object> stack=new Stack<object>();\r\n");
            sb.Append("private StringBuilder output;\r\n");
            sb.Append("private Func<Type, object> dependencyResolver;\r\n");
            sb.Append("public " + typeName + "(CmsRequestContext context, ParsingEngine parsingEngine, Func<Type, object> dependencyResolver) {\r\n");
            sb.Append("this.parsingEngine = parsingEngine;\r\n");
            sb.Append("this.context = context;\r\n");
            sb.Append("this.dependencyResolver = dependencyResolver;\r\n");

            sb.Append("}\r\n");
            sb.Append("public string Execute() {\r\n");
            sb.Append("return Execute(new Dictionary<string, object>());");
            sb.Append("}\r\n");
            sb.Append("public string Execute(Dictionary<string, object> variables) {\r\n");
            sb.Append("output = new StringBuilder();\r\n");
            sb.Append("var values = new Dictionary<string, object>();\r\n");
            sb.Append("var stack=new Stack<object>();\r\n");
            sb.Append("this.stack = stack;\r\n");
            sb.Append("if(this.context.Page != null)\r\n");
            sb.Append("stack.Push(this.context.Page);\r\n");
            sb.Append("var parserNamespace = \"\";\r\n");
            sb.Append("var parserName = \"\";\r\n");


            int parserIndex = 0;
            this.ParseElements(sb, this.parser.Parser.Document, ref parserIndex);

            sb.Append("return output.ToString();\r\n");
            sb.Append("}\r\n");
            sb.Append("public MethodWorx.CMS.Content.Page CurrentPage {\r\n");
            sb.Append("get { return this.stack.OfType<MethodWorx.CMS.Content.Page>().FirstOrDefault(); }\r\n");
            sb.Append("}\r\n");
            sb.Append("public MethodWorx.CMS.Content.ContentItem CurrentContent {\r\n");
            sb.Append("get { return this.stack.OfType<MethodWorx.CMS.Content.ContentItem>().FirstOrDefault(); }\r\n");
            sb.Append("}\r\n");
            sb.Append("public int N(object val) {\r\n");
            sb.Append("try { return (int)Convert.ChangeType(val, typeof(int)); } catch { throw new Exception(\"Could not convert to integer\"); }\r\n");
            sb.Append("}\r\n");
            sb.Append("public IEnumerable<object> Stack {\r\n");
            sb.Append("get { return this.stack; }\r\n");
            sb.Append("}\r\n");
            sb.Append("public void AddEditor(string name, MethodWorx.CMS.Content.Page page) {\r\n");
            sb.Append("output.Append(\"<div style='display:none' class='cms-editable-edit' cms_propertyName='\" + name + \"' cms_pageName='\" + page.Name + \"' cms_pageId='\" + page.Id.ToString() + \"'></div>\");\r\n");
            sb.Append("}\r\n");
            sb.Append("public void AddEditor(string name, MethodWorx.CMS.Content.ContentItem contentItem) {\r\n");
            sb.Append("output.Append(\"<div style='display:none' class='cms-editable-edit' cms_propertyName='\" + name + \"' cms_pageName='\" + contentItem.Page.Name + \"' cms_contentName='\" + contentItem.Name + \"' cms_pageId='\" + contentItem.Page.Id.ToString() + \"' cms_contentId='\" + contentItem.Id.ToString() + \"'></div>\");\r\n");
            sb.Append("}\r\n");
            sb.Append("}\r\n");
            sb.Append("}\r\n");

            return sb.ToString();
        }

        public string GetSource(PageTemplate template, PluginFactory plugins)
        {
            this.parser = new ParsingEngine(this.provider, template, plugins, null);
            var errors = this.parser.Parser.Document.GetErrors();

            if(errors.Any())
                throw new TemplateParsingException(errors.First().Error);

            string typeName = "template_" + DateTime.Now.Ticks.ToString();
            var code = this.ParseToCode(typeName);

            return code;
        }


        public Dictionary<string, string> GetPlaceholders(PageTemplate template, PluginFactory plugins)
        {
            var placeholders = new Dictionary<string, string>();
            //
            //  ok, this is actually quite easy really, look at the placeholders in the main template
            foreach(var t in template.GetAllInheritedTemplates(true))
            {
                // 
                //  ok, get the placeholders
                var xml = new XmlParser(t.Template);
                foreach(var node in xml.Document.FindElements("placeholder", true))
                {
                    if(node.HasAttribute("type", "content", true))
                    {
                        //  ok this is one
                        var name = node.GetAttributeValue("name", "", true);
                        if(!string.IsNullOrEmpty(name))
                        {
                            this.parser = new ParsingEngine(this.provider, template, plugins, null, name);
                            var errors = this.parser.Parser.Document.GetErrors();

                            if(errors.Any())
                                throw new TemplateParsingException(errors.First().Error);

                            string typeName = "template_" + DateTime.Now.Ticks.ToString();
                            var code = this.ParseToCode(typeName);

                            placeholders.Add(name.ToLower(), code);
                        }
                    }
                }
            }

            return placeholders;
        }
    }
}
